""" app.py - Application/game management using the event system.

    TODO: Document me!
    
    ~~~ Module Attributes ~~~
    
    default:    The first app created. Before any app is created, this
                will be None.
"""

import weakref
import operator

import pygame

# These constants are used by App.spiderpost_system_events to translate Pygame
# event types.
from pygame import (
    MOUSEBUTTONDOWN,
    MOUSEBUTTONUP,
    KEYDOWN,
    KEYUP,
    MOUSEMOTION,
    QUIT,
    K_ESCAPE
)
from pygame.event import (   
    get as pygame_event_get
)


from sbak.geometry import (
    Point,
    FrozenPoint,
    Rect
)
from sbak.video import (
    update as video_update,
    get_screen
)
from sbak import(
    error,
    event,
    resman,
    world
)
from sbak.event import (
    handler,
    Event,
    AbstractEventUser,
    EventHub
)

# These constants are used by App.spiderpost_system_events to translate Pygame
# event types.
from sbak.ui import (
    KeyPressEvent,
    KeyReleaseEvent,
    MousePressEvent,
    MouseReleaseEvent,
    MouseScrollEvent,
    MouseMoveEvent
)

#from sbak import entity

__all__ = (
    'App',
    'GUIApp',
    'Game',
    'AppExit'
)

# The default engine speed
DEFAULT_TICKS = 30

# See the attribute list at the top of this module.
default = None

class AppExit(Exception):
    " Signal to end an app's event loop. "
    pass

class QuitEvent(Event):
    """ Event that indicates the user is trying to quit the app. """
    signal = "quit"

class ControlEvent(Event):
    """ Event used to control the flow of an application. """
    signal = "control"
    
class CycleEvent(ControlEvent):
    """ Control event that signals the beginning of an event loop cycle.
    
        In the standard event cycle, this is followed by a series of
        system-produced events, such as keybord and mouse events, then an
        UpdateEvent.
    """
    signal = "cycle"
    
class UpdateEvent(ControlEvent):
    """ Control event that indicates that it is time for objects to update.
        
        In the standard event cycle, this is followed by a DisplayEvent.
    """
    signal = "update"

class DisplayEvent(ControlEvent):
    """ Control event that indicates it is time for objects to be displayed.
    
        DisplayEvent(subject[, root[, surface[, offset, [region]]]]) -> e
    
        surface:    The surface onto which things should display themselves.
                    If None is given (default), the screen is used.
        
        offset:     An offset. TODO: An offset for what?
        
        region:     A region. TODO: A region for what?
    
        In the standard event cycle, this is followed by a RefreshEvent.
    """
    
    __slots__ = ['surface', 'offset', 'region']
    
    signal = "display"
    
    def __init__(self, subject, root=None, surface=None, offset=(0,0),
                 region=None):
        ControlEvent.__init__(self, subject, root)
        self.surface = surface if surface is not None else get_screen()
        self.offset = offset
        self.region = region

class RefreshEvent(ControlEvent):
    """ Control event that indicates it is time to refresh the screen.
    
        In the standard event cycle, this is followed by a CycleEvent.
    """
    
    __slots__ = ['regions']
    
    signal = "refresh"
    
    def __init__(self, subject, root=None, regions=None):
        ControlEvent.__init__(self, subject, root)
        self.regions = regions

class App(AbstractEventUser):
    """ A general-purpose app.

        myapp = App(eventhub = None, resources = None)
        
        ~~~ Constructor Arguments ~~~

        eventhub:   An eventhub for all eventusers in the app. If None, a
                    new `sbak.event.EventHub` object will be created.

        resources:  A resource manager for children of the app to use
                    to load resources. If None (default), a
                    `ResourceManager` object is created for it.

        ~~~ Description ~~~

        TO DO: More documentation!

        This class defines functionality that is common to all app
        types.
        
        When an app is initialized it will hook a few handlers. These
        are listed below. Remember not to hook these handlers in
        subclasses or else they will be hooked twice, and will end up
        getting triggered twice everytime the corresponding event
        occurs.
        
        ~~~ EventHandlers ~~~
        
        Event           Handler         
        
        CycleEvent      on_cycle
        UpdateEvent     on_update
        DisplayEvent    on_display
        RefreshEvent    on_refresh
        QuitEvent       finish (can also be called directly without arguments)
        
        ~~~ Attributes ~~~
        
        resources:  A resource manager for children of the app to use
                    to load resources.
                    
        eventhub:   The eventhub for the app and all eventusers in it.
        
        ~~~ Notes ~~~
        
        TODO: Document how to properly implement an event handler loop.
    """
    
    __slots__ = ['_quit', 'resources']
    
    
    # Method autogeneration; see `sbak.base.LinkableM`
    __links = {'resources' : 'app'}
    
    
    def __init__(self, eventhub = None, resources = None ):
        global default
        
        AbstractEventUser.__init__(self)
        
        # If no default has yet been set, use self as the default.
        if default is None:
            default = self
        
        # Used to start/stop/finish the main event loop.
        self._quit = True
        
        # Create an eventhub if necessary
        if eventhub is None:
            self.eventhub = EventHub()
        else:
            self.eventhub = event.eventhub
        
        # Find out whether or not to create a resource manager.
        if resources is None:
            self.resources = resman.ResourceManager()
            self.resources.set_basics()
        else:
            self.resources = resources
    
    def get_root(self):
        return self
    
    # Temporary; will be moved to a better location once I determine where that
    # location is.
    _get_key_from_code = {
        K_ESCAPE : "escape",
    }.get
    
    def spiderpost_system_events(self):
        """ TODO: Posts all system events to the eventhub of the App.
        
            As the name suggests, this method gets each event that is
            currently on the Pygame system event queue and posts it
            into the queue of the App's eventhub, effectively moving the
            content of the system queue to the app's queue.
            
            This method is commonly used in the CE_CYCLE handler of
            apps as a means of making sure that system events only
            appear between the CE_CYCLE and--if present--CE_UPDATE
            event(s).
        """
        for e in pygame.event.get():
            
            etype = e.type
            
            # KEYDOWN --> KeyPressEvent
            if etype == KEYDOWN:
                self.spiderpost(KeyPressEvent, self,
                    key = self._get_key_from_code(e.key, "unknown"),
                    code = e.key,
                    # TODO: Key modifier flags
                )
            
            # KEYUP --> KeyReleaseEvent
            elif etype == KEYUP:
                self.spiderpost(KeyReleaseEvent, self,
                    key = self._get_key_from_code(e.key, "unknown"),
                    code = e.key,
                    # TODO: Key modifier flags
                )
            
            # MOUSEBUTTONDOWN --> MousePressEvent/MouseScrollEvent
            elif etype == MOUSEBUTTONDOWN:
                btn = e.button - 1
                # Buttons 3 and 4 are actually scroll wheel movements.
                if btn == 3:
                    self.spiderpost(MouseScrollEvent, self,
                                    pos = Point(e.pos), val = -1)
                elif btn == 4:
                    self.spiderpost(MouseScrollEvent, self,
                                    pos = Point(e.pos), val = 1)
                # All other buttons are assumed to be actual buttons.
                else: # 0-2, 5+
                    self.spiderpost(MousePressEvent, self,
                                    pos = Point(e.pos), button = btn)
            
            # MOUSEBUTTONUP --> MouseReleaseEvent
            elif etype == MOUSEBUTTONUP:
                btn = e.button
                if btn not in (3,4):
                    self.spiderpost(MousePressEvent, self,
                                    pos = Point(e.pos), button = btn)
            
            # MOUSEMOTION --> MouseMoveEvent
            elif etype == MOUSEMOTION:
                self.spiderpost(MouseMoveEvent, self,
                    pos = Point(e.pos),
                    rel = Point(e.rel),
                    buttons = [n for n, b in enumerate(e.buttons) if b]
                )
            
            # QUIT --> QuitEvent
            elif etype == QUIT:
                self.spiderpost(QuitEvent, self)
            
            # TODO: Other event types.
    
    def on_cycle(self, hook, e):
        """ CycleEvent handler for App.
        
            Spiderposts an UpdateEvent, unless App.finish has been called,
            in which case AppExit is raised to end the loop.
        
            Also, this spiderposts events from the system. The
            `spiderpost_system_events` method is used to do this.
        
            Posting a CycleEvent will cause the main loop to be
            perpetuated after it has started. Calling `App.finish`
            sets an internal flag which causes this handler method
            to raise an AppExit exception, thereby breaking out of
            the main loop.
        """
        if self._quit:
            raise AppExit()
        self.spiderpost_system_events()
        self.spiderpost(UpdateEvent, self)
    
    on_cycle.priority = 0
    
    def on_update(self, hook, e):
        """ UpdateEvent handler for App.
        
            Spiderposts a DisplayEvent for the whole screen.
        """
        self.spiderpost(DisplayEvent, self, get_screen())
    
    on_update.priority = 0
    
    def on_display(self, hook, e):
        """ DisplayEvent handler for App.
        
            Spiderposts a RefreshEvent for the whole screen.
        """
        
        self.spiderpost(RefreshEvent, self)
    
    on_display.priority = 0
    
    def on_refresh(self, hook, e):
        """ RefreshEvent handler for App.
        
            This will update the screen with the regions indicated by
            the event, then spiderpost a CycleEvent to continue the
            control cycle.
        """
        regions = e.regions
        if regions is not None:
            video_update(regions)
        else:
            video_update()
        self.spiderpost(CycleEvent, self)
    
    on_refresh.priority = 0
    
    def start(self, startpoint = CycleEvent):
        """ Starts (or re-starts) the main event loop.
        
            If the app is already started, this does nothing.
        """
        if self._quit:
            self._quit = False
            self.spiderpost(CycleEvent, self)
            try:
                self.eventhub.dispatch()
            except AppExit:
                pass
    
    @handler(QuitEvent, priority=0)
    def finish(self, hook=None, e=None):
        """ Causes the main loop to end after the current cycle ends.
        
            NOTE: In order for this method to work, one of the event
            handlers used in the app's main event loop must raise
            `sbak.error.AppExit` when it detects that the app's
            internal attribute `_quit` is nonzero.
            
            The event handlers of both GUIApp and Game implement this
            behavior correctly. (FIXME: Unverified.)
            
            This method can be used as an event handler.
        """
        self._quit = True
    
    def stop(self):
        """ Stops the app's main event loop immediately.
            
            The loop will stop at the position in the event queue that
            is currently being handled. Hooks that have not been called
            yet will not be called, and unhandled events will remain in
            the queue for when the app is started again.
            
            If the app is already stopped, nothing happens.
            
            TIP: If you call this method from inside a control event
            handler that is hooked with a higher priority than the
            app's default handler for that event, the main event loop
            can be started right where it left off simply by calling
            app_obj.start(None).
            
            FIXME: This method will fail outside the main loop.
        """
        if not self._quit:
            self._quit = True
            raise AppExit()

class Game(App):
    __doc__ =\
    """ Game application manager.
    
        mygame = Game(ticks = %(DEFAULT_TICKS)s, resources = None, universe = None)
    
        ~~~ Constructor Arguments ~~~
        
        ticks:      Ticks per second. The Game will try to have no more
                    main loop cycles per second than this value (though
                    there may be less). The default is %(DEFAULT_TICKS)s.
        
        eventhub:   An eventhub for the game and everything in it. If None is
                    given (default), a new `sbak.event.EventHub` is created.
        
        resources:  A resource manager for worlds, entities, and other
                    objects to use to load resources dynamically.
                    If None (default), a `sbak.resman.Resource` object
                    will be created for it.
        
        universe:   A universe to hold worlds. All worlds in the game
                    should go in here when loaded. If None is passed,
                    a `sbak.world.Universe` object is created for it.
                    This universe can later be accessed as
                    `mygame.universe`.
        
        ~~~ Description ~~~
        
        TO DO: More documentation!
        
        Games hook their .finish method to the KEYDOWN/K_ESCAPE
        event and the QUIT event. This allows the game to be quit at
        any time by pressing the Escape key or the window close button.
    
        ~~~ Attributes ~~~
        
        ticks:  The number of ticks per second. This will determine
                how many times the game is updated every second.
                This effectively determines the target FPS as well.
        
        clock:  The clock used to regulate game speed.
        
        resources:  The resource manager used by objects of the game.
        
        universe:   Container of all worlds active in the game.
        
    """ % { 'DEFAULT_TICKS' : DEFAULT_TICKS }
    
    def __init__( self, ticks = DEFAULT_TICKS,
                        eventhub = None,
                        resources = None,
                        universe = None):
        App.__init__(self, eventhub, resources)
        
        # Time regulation
        
        self.clock = pygame.time.Clock()
        self.ticks = ticks
        
        # Link with the universe
        if universe is None:
            self.universe = world.Universe()
        else:
            self.universe = universe

    def forward_spider(self, e):
        super(Game,self).forward_spider(e)
        uni = self.universe
        if uni is not None:
            uni.forward_spider(e)
    
    def unify_eventhub(self, hub):
        super(Game,self).unify_eventhub(hub)
        uni = self.universe
        if uni is not None:
            uni.unify_eventhub(hub)

    # Event handlers
    
    def on_cycle(self, hook, e):
        """ Game's CycleEvent handler.
        
            Regulates game speed using a Pygame Clock, posts system
            events into the Game's queue, and spiderposts a UpdateEvent.
            
            If AbstractApp.finish() has been called, this will raise
            sbak.error.GameExit, which will cause the control cycle
            to end before any of the above can occur.
        """
        super(Game,self).on_cycle(hook, e)
        self.clock.tick(self.ticks)
    
    on_cycle.priority = 0
    
    def show_fps(self, val=True, pos=(0,0), bgcolor=(0,0,0),
                 fgcolor=(255,255,255),
                 priority = 1000000):
        """ Call this to create a hook that will draw the game's FPS.
        
            val:    Whether or not to show the FPS. If True (default),
                    the hook is created (unless it already exists). If
                    False, the hook is removed (unless it does not
                    exist.) Defaults to True.
            
            pos:    The position on-screen at which the FPS is drawn.
                    Defaults to 0,0.
            
            bgcolor:The background of the text. Defaults to black.
            
            fgcolor:The color of the text. Defaults to white.
            
            priority:
                    Priority of the draw hook. Defaults to 1000000,
                    which is usually high enough to guaruntee that the
                    FPS is always drawn on top of everything else.
                    
            Since many game developers will want to see the FPS while
            developing their game, this method is here to provide an
            easy way to do so.
            
            Calling this function with the default values will create a
            callback that will be hooked to the CE_DISPLAY control
            event handler of the game's event manager. The default
            values will put the FPS in the top-left corner of the
            screen with white text on a black background with a very
            large priority number so that it will almost always appear
            on top of everything else.
            
            To hide the FPS later, call the function again with False
            as the first and only argument.
        """
        # If this is a call to show the FPS, create and hook the FPS callback.
        if val:
            # If an fps hook already exists, remove it.
            if hasattr(self, '_showfpshook'):
                self._showfpshook.remove()
                del self._showfpshook
            
            # Get needed stuff from draw module.
            from sbak.draw import text as draw_text, fix_color
            
            # Fix the arguments.
            pos = map(int, pos[:2])
            fgcolor = fix_color(fgcolor)
            bgcolor = fix_color(bgcolor)
        
            # Create the display callback.
            def display_fps(hook, e):
                fps = self.clock.get_fps()
                ticks = self.ticks
                perc = fps / ticks
                text = "FPS = %.1f / %.1f = %.1f%%" % (fps, ticks, perc)
                
                region = draw_text(e.dest, pos, text, None, fgcolor, bgcolor)
                self.smudge(region)
        
            # Create the hook.
            self._showfpshook = self.hook(display_fps, DisplayEvent,
                priority = priority)
        
        # If this is a call to hide the FPS and an FPS displayer exists,
        # remove it.
        elif hasattr(self, "_showfpshook"):
            self._showfpshook.remove()
            del self._showfpshook